iT邦幫忙

DAY 1
7

Reactjs 30 天邊做邊學系列系列 第 1

Reactjs Day 1 - 前言與理解 Function.prototype.bind

  • 分享至 

  • xImage
  •  

前言
自從 2013 年 Reactjs 開放原始碼後就被其單純的架構和理念所吸引。但由於沒有迫切的需求一直沒有將其使用于產品中。
且在當時 Angular 正流行,大部份的人都被 two-way binding 神奇簡練的寫法給嚇傻了,相較之下在第一眼看到 Reactjs 其給人的感受為: 怎麼好像要多打很多程式碼啊!而在 2014 今年因為 Facebook 提出 Flux 與 Jest 等東西的使我決定認真的研究一下並將其使用在產品上。
但由於 React 相關文章大多很散且中文文章不多,所以接下來的 30 天我將逐步根據官網教學與一些國內外高手的文章心得,佐以實作驗證後,記錄為此系列文章。

準備
關於第一個部分,我打算會列出一些網路資源與一點點使用 React 時重要的 Javascript 觀念。

理解 Javascript 的 Function.prototype.bind
大部分 Javascript 的介紹您都可以輕易的在網站上找到,不過在學習過程中我覺得特別需要提到的是關於 bind() 的用法,因為在 React 中其實蠻容易遇到的,所以一開始我希望針對這部分做個簡單的介紹。第二天開始我們將從 Getting Started 逐步介紹。

Function.prototype.bind 函式繫結大概是當您開始學習 Javascript 時最後關注到的議題。通常是當您遇到一種狀況:需要在其他 function 保留 this 的執行環境(Context)。講執行環境可能太抽象,舉例來說就是當您需要在函式中的另外一個函式中呼叫 this.action() 的時候。(這邊如果看不懂請耐著性子看下去),不過通常這時您可能也不知道您需要的就是 Function.prototype.bind()

第一次您遇到上述的問題,您可能會傾向于把 this 儲存成一個變數,接著即便您切換了 Context 還是可以參考到這個物件。
如果您看不懂上面在說什麼,請先參考這篇文章。許多人會採用 self, _this 或者 context 當作變數名稱,並且把 this 放進去。這些方法都是可行的且並沒有什麼不妥,但有一個更不錯的方式。

我們實際上要解決的問題是?
下面有一份簡單的範例程式碼,情況是某人忘記把 Context 保留下來:

/**
 * 我們舉例一個機器人物件,機器人有一些基本的 function 來執行動作
 * 不過問題是當我們要求機器人執行動作的時候,他需要先到確定還有沒有能量。
 * 
 *
 * Note: 這段程式碼只是希望能夠用具像化一點的比喻來說明。
 */
var Robot = {
  /** private */
  power: 100,

  walk: function () {
    console.log('Robot walked');
    this.power -= 10;
  },

  fly: function () {
    console.log('Robot flied');
    this.power -= 20;
  },

  check: function (excute) {
    if (this.power > 0)
      excute();
  },
  
  /** public */
  showoff: function () {
    this.check(function () {
      this.walk(); /* 實際執行的動作。 */
      this.fly();
    })
  }
}

Robot.showoff();

如果照著上面把實際要執行的動作當作 callback 傳給 check(),當您要再次呼叫 this.walk() 的時候
就會發現出現錯誤訊息

TypeError: Object #<Object> has no method 'walk'

這是因為我們再次傳進去的匿名函式不知道關於 this 的東西,在這裏我們並沒有善用閉包來保存 Context。
而對很多人可能就會把上面的範例修改為如下

var Robot = {
  /** private */
  power: 100,

  walk: function () {
    console.log('Robot walked');
    this.power -= 10;
  },

  fly: function () {
    console.log('Robot flied');
    this.power -= 20;
  },

  check: function (excute) {
    if (this.power > 0)
      excute();
  },
  
  /** public */
  showoff: function () {
    var that = this;
    this.check(function () {
      that.walk(); /* 實際執行的動作。 */
      that.fly();
    })
  }
}

Robot.showoff();

宣告成區域變數之後,閉包就會幫助我們記住這個 Context,這也是相對直覺的方式,同上面說的這樣做並沒有任何不妥。
不過我們知道了一件事,就是我們需要保存 Robot 這個物件參考的 Context,給 Callback ,即範例中的 excute。
當我們呼叫 that.walk() 的時候其實就是在使用閉包。根據 MDN 說明,其實閉包就是一個特殊的物件,它有兩個含義:

  1. 它是一個 function。
  2. 它產生了一個 Context ,概略的說就是幫你記錄上一層有宣告的變數。

這裏就不詳細說明關於閉包,不理解的推薦這篇文章這篇

閉包的方式已經可以運作了,但是我們覺得他不夠漂亮,因此我們就來使用 Function.prototype.bind()

讓我們來重構上面的程式範例

var Robot = {
  /** private */
  power: 100,

  walk: function () {
    console.log('Robot walked');
    this.power -= 10;
  },

  fly: function () {
    console.log('Robot flied');
    this.power -= 20;
  },

  check: function (excute) {
    if (this.power > 0)
      excute();
  },
  
  /** public */
  showoff: function () {
    // var that = this;
    this.check(function () {
      this.walk();
      this.fly();
    }.bind(this));
  }
}

Robot.showoff();

我們剛剛做了什麼??
當我們呼叫了 .bind() 的時候,其實它非常單純的建立了一個新的 function,只不過這個 function 把 this 的值綁定進去。
所以我們同時把我們想要的 Context(即 Robot 物件) 給保存了下來。接著當我們的回呼函式在執行的時候 this 就是參考到 Robot 這個物件。

如果你有興趣了解 Function.prototype.bind() 內部運行機制,他看起來大概就像下面這樣

Function.prototype.bind = function (scope) {
    var fn = this;
    return function () {
        return fn.apply(scope);
    };
}

接著我們再來看看一個非常簡單的案例:

var foo = {
    x: 3
}

var bar = function () {
  console.log(this.x);
}

bar(); // undefined

var boundFunc = bar.bind(foo);

boundFunc(); // 3

這個範例是說,bind() 幫我們建立了一個新的 function ,並且當我們執行時這個 function 的 this 是指向 foo,而不是全域。
如果您還是不清楚可以大略理解為:把 function 掛到某個物件底下(當然不是真的加進去),只是這樣一來可以透過 this 取得該物件的 Context。

實務應用
當我們學習某些東西時,我不只需要理解觀念,也會試著將其套用在實務上以驗證自己是否明白。來看看一些實務上的應用吧!

Click 事件處理
其中一個用途是拿它來追蹤點擊次數,然後可能是要把它存在某個物件裡類似下面這樣

var logger = {
  x: 0,
  increment: function () {
    this.x++;
    console.log(this.x);
  }
}

然後指派一個按鈕的 Click 處理函式去呼叫 logger 物件

document.querySelector('button').addEventListener('click', function () {
  logger.increment();
});

不過上面這種做法,我們已經建立了一個不必要的匿名函式,並且因為這個匿名函式使用了 logger 呼叫了 increment() 所以產生了一個閉包用以確保了 this 是正確的參考物件。
不明白!?再看看下面這個最根本的寫法吧

document.querySelector('button').addEventListener('click', logger.increment); // NaN

原本是這樣的,當你 Click 的時候執行一個 function ,不過如果你用上面這種寫法的話,意思是你只是把 function 傳進去,根據 this 的定義他其實是指的是誰(哪個物件)呼叫這個函式 this 就是指向它。
而這裡呼叫的人根本不是 logger 這個物件。也因此使用了一個匿名函式,建立了一個閉包,就是為了保留住 logger 物件的狀態。

好了!講完上面這些我們來看看更乾淨的寫法:

document.querySelector('button').addEventListener('click', logger.increment.bind(logger)); 

現在,對於 bind() 應該比較不陌生了吧!因為在 React 中蠻多機會使用 bind()。如官方的範例:

componentDidMount: function() {
  $.ajax({
    url: this.props.url,
    dataType: 'json',
    success: function(data) {
      this.setState({data: data});
    }.bind(this),
    error: function(xhr, status, err) {
      console.error(this.props.url, status, err.toString());
    }.bind(this)
  });
}

資源列表
官方網站
React.NET
Sebastian Gist
ReactJS.tw 台灣粉絲團
React揭秘
React Components


下一篇
Reactjs Day 2 - 快速概覽起手式
系列文
Reactjs 30 天邊做邊學系列30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中
0
wordsmith
iT邦高手 1 級 ‧ 2014-09-16 23:21:24

很棒的主題,希望能透過Andy的介紹,更了解 Reactjs

0
andyyu0920
iT邦研究生 4 級 ‧ 2014-09-17 07:33:04

謝謝您的支持,哈哈希望能撐完啊

0
appleboy
iT邦新手 4 級 ‧ 2014-09-23 13:45:05

感謝分享,終於有人寫 ReactJS 了

0
andyyu0920
iT邦研究生 4 級 ‧ 2014-09-24 10:53:58

感謝支持XD 如果寫的不好請指教

0
daith
iT邦新手 5 級 ‧ 2014-09-24 12:48:20

謝謝 安迪哥的分享!!
其他後面的文章~

0
pajace2001
iT邦研究生 1 級 ‧ 2014-10-07 08:59:50

請問你程式碼是怎麼貼的呢?新版的介面我找不到貼程式碼的地方 XD

0
roger03
iT邦新手 4 級 ‧ 2014-10-07 16:18:26

給樓上,

編輯器的倒數第二個元件便是插入原始碼功能 ^^

0
andyyu0920
iT邦研究生 4 級 ‧ 2014-10-08 09:03:02

我一樣用 BBCode 不過有些不支援就是了

0
stan926
iT邦新手 5 級 ‧ 2014-11-30 18:14:47

寫得真好~這教學讓我把閉包的觀念弄得更清楚了,謝謝

0
thula413
iT邦新手 5 級 ‧ 2015-02-12 16:42:23

很棒!謝謝分享。

0
larryyangsen
iT邦新手 5 級 ‧ 2015-03-10 09:43:43

感謝分享,剛來好好學些前端的技術了

我要留言

立即登入留言